package pl.edu.pb.wi.pwnography.modules;

import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

public class Discretization {
    private static final Logger log = Logger.getLogger(Discretization.class
	    .getName());
    private static final String PREFIX_BASE = "DISCRETIZATION_";
    private static final String EQUAL_LENGTH_PREFIX = "EQUAL_SIZE_";
    private static final String INTERVALS_SUFFIX = "_INTERVALS";
    private static final String LARGEST_CLASSES_PREFIX = "LARGEST_CLASS_";
    private static final String CLASSES_SUFFIX = "_CLASSES";

    /*
     * Dykretyzacja oparta o przedziały równej długości. Jedynie kolumny
     * zawierające wartości typu Float mogą zostać potraktowane tą metodą.
     */
    public static Map<String, List<Object>> equalSize(String columnName,
	    List<Object> columnValues, int numberOfIntervals,
	    String newColumnName) throws Exception {
	if (columnValues.size() < 1) {
	    return null;
	}
	if (numberOfIntervals < 1) {
	    throw new Exception("Number of intervals can't be less than 1.");
	}

	/*
	 * Sprawdzenie czy każda z wartości kolumny da się zrzutować na typ
	 * Float.
	 */
	for (Object o : columnValues) {
	    if (!(o instanceof Float)) {
		throw new Exception(
			String.format(
				"Value [%s] isn't Float type! Fix column [%s]. Equal size discetization can be used only on Float type columns.",
				o.toString(), columnName));
	    }
	}

	/*
	 * Wyliczenie wartości minimalne, maksymalnej oraz skoku. Skok jest to
	 * różnica wartości minimalnej oraz maksymalnej podzielona przez ilość
	 * interwałów.
	 */
	Float minimumValue = (Float) columnValues.get(0);
	Float maximumValue = (Float) columnValues.get(0);
	for (Object value : columnValues) {
	    Float tempValue = (Float) value;
	    if (tempValue > maximumValue) {
		maximumValue = tempValue;
	    }
	    if (tempValue < minimumValue) {
		minimumValue = tempValue;
	    }
	}
	Float intervalValue = (maximumValue - minimumValue) / numberOfIntervals;

	List<Object> discretizedValues = new LinkedList<Object>();
	for (Object o : columnValues) {
	    Float value = (Float) o;

	    /*
	     * Jeżeli wartość mieści się w pierwszym przedziale od razu
	     * przydziel wartość "1"
	     */
	    if (value >= minimumValue && value < (minimumValue + intervalValue)) {
		discretizedValues.add(1f);
		log.info(String.format("Added %f to %d section.", value, 1));
		continue;
	    }

	    /*
	     * Jeżeli wartość mieści się w ostatnim przedziale od razu przydziel
	     * wartość równą ilości przedziałów.
	     */
	    if (value >= (minimumValue + ((numberOfIntervals - 1) * intervalValue))
		    && value <= maximumValue) {
		discretizedValues.add(Float.valueOf(numberOfIntervals));
		log.info(String.format("Added %f to %d section.", value,
			numberOfIntervals));
		continue;
	    }

	    /*
	     * W przeciwnym przypadku wyszukaj numer przedziału.
	     */
	    for (int i = 1; i < numberOfIntervals; ++i) {
		if (value >= (minimumValue + (i * intervalValue))
			&& value < (minimumValue + ((i + 1) * intervalValue))) {
		    log.info(String.format("Added %f to %d section.", value, i));
		    discretizedValues.add(new Float(i + 1));
		    break;
		}
	    }
	}

	if (columnValues.size() != discretizedValues.size()) {
	    throw new Exception(
		    String.format(
			    "Discretized column don't have same amount of objects as input column. Input: %d Output: %d",
			    columnValues.size(), discretizedValues.size()));
	}

	/*
	 * Tworzenie nazwy nowej kolumny oraz umieszczenie jej razem z
	 * wartościami w HashMapie.
	 */
	String prefixedName;

	if (newColumnName == null || newColumnName.isEmpty()) {
	    prefixedName = new StringBuilder(PREFIX_BASE)
		    .append(EQUAL_LENGTH_PREFIX).append(columnName).append("_")
		    .append(Integer.toString(numberOfIntervals))
		    .append(INTERVALS_SUFFIX).toString();
	} else {
	    prefixedName = newColumnName;
	}

	Map<String, List<Object>> namedDiscretizedColumn = new LinkedHashMap<String, List<Object>>();
	namedDiscretizedColumn.put(prefixedName, discretizedValues);

	return namedDiscretizedColumn;
    }

    /*
     * Dykretyzacja oparta o preferowanie najliczniejszych klas. Można nią
     * potraktować dowolną kolumnę, zarówno typu String jak i Float.
     */
    public static Map<String, List<Object>> largestClass(String columnName,
	    List<Object> columnValues, int numberOfClasses, String newColumnName)
	    throws Exception {
	if (numberOfClasses < 1) {
	    throw new Exception("Number of classes can't be less than 1.");
	}

	/*
	 * Oblicz ilość wystąpień każdej z wartości.
	 */
	Map<Object, Integer> countValues = new LinkedHashMap<Object, Integer>();
	List<Object> discretizedValues = largestClass(columnValues,
		numberOfClasses, countValues);

	if (numberOfClasses > countValues.keySet().size()) {
	    numberOfClasses = countValues.keySet().size();
	}
	/*
	 * Tworzenie nazwy nowej kolumny oraz umieszczenie jej razem z
	 * wartościami w HashMapie.
	 */
	String prefixedName;
	if (newColumnName == null || newColumnName.isEmpty()) {
	    prefixedName = new StringBuilder(PREFIX_BASE)
		    .append(LARGEST_CLASSES_PREFIX).append(columnName)
		    .append("_").append(Integer.toString(numberOfClasses))
		    .append(CLASSES_SUFFIX).toString();
	} else {
	    prefixedName = newColumnName;
	}

	Map<String, List<Object>> namedDiscretizedColumn = new LinkedHashMap<String, List<Object>>();
	namedDiscretizedColumn.put(prefixedName, discretizedValues);

	return namedDiscretizedColumn;
    }

    public static List<Object> largestClass(List<Object> columnValues,
	    int numberOfClasses, Map<Object, Integer> countValues) {
	for (Object o : columnValues) {
	    Integer valueCount = countValues.get(o);
	    if (valueCount == null) {
		countValues.put(o, new Integer(1));
		continue;
	    } else {
		countValues.put(o, ++valueCount);
	    }
	}

	/*
	 * Sprawdź czy ilość klas nie jest większa niż aktualna ilość klas w
	 * kolumnie.
	 */
	int numberOfClassesTmp = numberOfClasses;
	if (numberOfClassesTmp > countValues.keySet().size()) {
	    log.info(String
		    .format("Couldn't create [%d] classes. There's only [%d] kinds of distinct values in this column.",
			    numberOfClassesTmp, countValues.keySet().size()));
	    numberOfClassesTmp = countValues.keySet().size();
	}

	/*
	 * Tworzenie listy wszystkich ilości wystąpień oraz sortowanie ich w
	 * malejącym porządku.
	 */
	Set<Integer> uniqueCounts = new HashSet<Integer>(countValues.values());
	List<Integer> sortedCounts = new LinkedList<Integer>(uniqueCounts);
	Collections.sort(sortedCounts);
	Collections.reverse(sortedCounts);

	/*
	 * Tworzenie kolumny z nowymi wartościami (wartościami klas).
	 */
	List<Object> discretizedValues = new LinkedList<Object>();
	for (Object o : columnValues) {
	    Integer instancesCount = countValues.get(o);
	    Float classNumber = Float.valueOf(sortedCounts
		    .indexOf(instancesCount) + 1);

	    if (classNumber > numberOfClassesTmp) {
		discretizedValues.add(Float.valueOf(numberOfClassesTmp + 1));
	    } else {
		discretizedValues.add(classNumber);
	    }
	}
	return discretizedValues;
    }

}
